﻿// <copyright file="HSL.cs" company="Guillaume Leparmentier">
// Copyright © 2007 Guillaume Leparmentier
// </copyright>
// <date>2007-06-04</date>
// <adapted>Oleg V. Polikarpotchkin</adapted>
// <adapt-date>2009-06-08</adapt-date>
// <email>ov-p@yandex.ru</email>
// <summary>OpenWPFChart library.
// HSL Color Model Structure.
// </summary>
// <revision>$Id$</revision>

using System;
using System.Text;
using System.ComponentModel;
using System.Windows.Media;

namespace OpenWPFChart.Helpers.ChooseColorDialog
{
	/// <summary>
	/// HSL Color Model Structure.
	/// </summary>
	/// <remarks>
	/// HSL is similar to HSB. The main difference is that HSL is symmetrical to lightness and 
	/// darkness. This means that: 
	/// <list type="bullet">
	/// <item>In HSL, the Saturation component always goes from fully saturated color to the 
	/// equivalent gray (in HSB, with B at maximum, it goes from saturated color to white).</item>
	/// <item>In HSL, the Lightness always spans the entire range from black through the chosen 
	/// hue to white (in HSB, the B component only goes half that way, from black to the chosen 
	/// hue).</item>
	/// </list>
	/// <para>Borrowed from Guillaume Leparmentier "Manipulating colors in .NET" article;
	/// see http://www.codeproject.com/KB/recipes/colorspace1.aspx.</para>
	/// </remarks>
	public struct HSL : INotifyPropertyChanged, IDataErrorInfo
	{
		/// <summary>
		/// Empty HSL structure;
		/// </summary>
		public static readonly HSL Empty = new HSL();

		/// <summary>
		/// Initializes a new instance of the <see cref="HSL"/> struct.
		/// </summary>
		/// <param name="a">Alpha channel value.</param>
		/// <param name="h">Hue value.</param>
		/// <param name="s">Saturation value.</param>
		/// <param name="l">Luminance value.</param>
		public HSL(double a, double h, double s, double l) : this()
		{
			alpha = a;
			hue = h;
			saturation = s;
			luminance = l;
		}
		/// <summary>
		/// Initializes a new instance of the <see cref="HSL"/> struct.
		/// </summary>
		/// <param name="h">Hue value.</param>
		/// <param name="s">Saturation value.</param>
		/// <param name="l">Luminance value.</param>
		public HSL(double h, double s, double l) : this(1, h, s, l)
		{
		}
		/// <summary>
		/// Initializes a new instance of the <see cref="HSL"/> struct.
		/// </summary>
		/// <param name="color">The color.</param>
		public HSL(Color color) : this()
		{
			double r = color.R / 255.0;
			double g = color.G / 255.0;
			double b = color.B / 255.0;

			double max = Math.Max(r, Math.Max(g, b));
			double min = Math.Min(r, Math.Min(g, b));

			// Hue.
			double h = 0.0;
			if (min != max)
			{
				if (max == r && g >= b)
					h = 60 * (g - b) / (max - min);
				else if (max == r && g < b)
					h = 60 * (g - b) / (max - min) + 360;
				else if (max == g)
					h = 60 * (b - r) / (max - min) + 120;
				else if (max == b)
					h = 60 * (r - g) / (max - min) + 240;
			}

			// Luminance.
			double l = (max + min) / 2.0;

			// Saturation.
			double s = 0;
			if (l == 0 || max == min)
				s = 0;
			else if (0 < l && l <= 0.5)
				s = (max - min) / (max + min);
			else if (l > 0.5)
				s = (max - min) / (2 - (max + min)); //(max-min > 0)?

			alpha = color.A / 255.0;
			hue = h;
			saturation = s;
			luminance = l;
		}

		#region Color Properties
		private double alpha;
		/// <summary>
		/// Gets or sets the Alpha component.
		/// </summary>
		/// <remarks>
		/// The transparence of this Color. The saturation transparence from 0.0 through 1.0, where 0.0 
		/// is transparent and 1.0 is opaque.
		/// </remarks>
		public double Alpha
		{
			get { return alpha; }
			set 
			{
				if (alpha != value)
				{
					alpha = value;
					NotifyPropertyChanged("Alpha");
				}
			}
		}

		private double hue;
		/// <summary>
		/// Gets or sets the hue component.
		/// </summary>
		/// <remarks>
		/// The hue, in degrees, of this Color. The hue is measured in degrees, ranging from 0.0 
		/// through 360.0, in HSL color space. Each value corresponds to one color : 0 is red, 
		/// 45 is a shade of orange and 55 is a shade of yellow.
		/// </remarks>
		public double Hue
		{
			get { return hue; }
			set
			{
				if (hue != value)
				{
					hue = value;
					NotifyPropertyChanged("Hue");
				}
			}
		}

		private double saturation;
		/// <summary>
		/// Gets or sets Saturation component.
		/// </summary>
		/// <remarks>
		/// Variation of the color depending on the Luminance.
		/// <para>Ranges from 0 to 1 (from the center of the black&white axis).</para>
		/// </remarks>
		public double Saturation
		{
			get { return saturation; }
			set
			{
				if (saturation != value)
				{
					saturation = value;
					NotifyPropertyChanged("Saturation");
				}
			}
		}

		private double luminance;
		/// <summary>
		/// Gets or sets the Luminance component.
		/// </summary>
		/// <remarks>
		/// Also known as Lightness or Luminosity or Intensity.
		/// <para>Ranges from 0 to 1 (from black to white).</para>.
		/// </remarks>
		public double Luminance
		{
			get { return luminance; }
			set
			{
				if (luminance != value)
				{
					luminance = value;
					NotifyPropertyChanged("Luminance");
				}
			}
		}
		#endregion Color Properties

		/// <summary>
		/// Performs an implicit conversion from <see cref="ovp.WPFGears.HSL"/> to 
		/// <see cref="System.Windows.Media.Color"/>.
		/// </summary>
		/// <param name="hsb">The HSL.</param>
		/// <returns>The result of the conversion.</returns>
		public static implicit operator Color(HSL hsl)
		{
			double r = 0;
			double g = 0;
			double b = 0;

			if (hsl.Saturation == 0)
			{ // achromatic color (gray scale)
				r = g = b = hsl.Luminance;
			}
			else
			{
				double h = hsl.Hue;
				double s = hsl.Saturation;
				double l = hsl.Luminance;

				double q = (l < 0.5) ? (l * (1.0 + s)) : (l + s - (l * s));
				double p = (2.0 * l) - q;

				double Hk = h / 360.0;
				double[] T = new double[3];
				T[0] = Hk + (1.0 / 3.0);	// Tr
				T[1] = Hk;				// Tb
				T[2] = Hk - (1.0 / 3.0);	// Tg

				for (int i = 0; i < 3; i++)
				{
					if (T[i] < 0) T[i] += 1.0;
					if (T[i] > 1) T[i] -= 1.0;

					if ((T[i] * 6) < 1)
						T[i] = p + ((q - p) * 6.0 * T[i]);
					else if ((T[i] * 2.0) < 1) //(1.0/6.0)<=T[i] && T[i]<0.5
						T[i] = q;
					else if ((T[i] * 3.0) < 2) // 0.5<=T[i] && T[i]<(2.0/3.0)
						T[i] = p + (q - p) * ((2.0 / 3.0) - T[i]) * 6.0;
					else 
						T[i] = p;
				}

				r = T[0];
				g = T[1];
				b = T[2];
			}

			return Color.FromArgb((byte)(hsl.Alpha * 255), (byte)(r * 255)
				, (byte)(g * 255), (byte)(b * 255));
		}

		/// <summary>
		/// Indicates whether this instance and a specified object are equal.
		/// </summary>
		/// <param name="obj">Another object to compare to.</param>
		/// <returns>
		/// <c>true</c> if <paramref name="obj"/> and this instance are the same type and represent 
		/// the same value; otherwise, <c>false</c>.
		/// </returns>
		public override bool Equals(Object obj)
		{
			if (obj == null || GetType() != obj.GetType()) 
				return false;

			return (this == (HSL)obj);
		}

		/// <summary>
		/// Implements the operator ==.
		/// </summary>
		/// <param name="hsl1">First instance.</param>
		/// <param name="hsl2">Second instance.</param>
		/// <returns>The result of the operator.</returns>
		public static bool operator==(HSL hsl1, HSL hsl2)
		{
			return (hsl1.Alpha == hsl2.Alpha
				&& hsl1.Hue == hsl2.Hue
				&& hsl1.Saturation == hsl2.Saturation
				&& hsl1.Luminance == hsl2.Luminance);
		}

		/// <summary>
		/// Implements the operator !=.
		/// </summary>
		/// <param name="hsl1">First instance.</param>
		/// <param name="hsl2">Second instance.</param>
		/// <returns>The result of the operator.</returns>
		public static bool operator!=(HSL hsl1, HSL hsl2)
		{
			return !(hsl1 == hsl2);
		}

		/// <summary>
		/// Returns the CSV string of values of this instance components.
		/// </summary>
		/// <returns>
		/// A CSV <see cref="T:System.String"/> of values of this instance components.
		/// </returns>
		public override string ToString()
		{
			return string.Format("({0},{1},{2},{3})"
				, Alpha, Hue, Saturation, Luminance);
		}

		/// <summary>
		/// Returns the hash code for this instance.
		/// </summary>
		/// <returns>
		/// A 32-bit signed integer that is the hash code for this instance.
		/// </returns>
		public override int GetHashCode()
		{
			return Alpha.GetHashCode() ^ Hue.GetHashCode() 
				^ Saturation.GetHashCode() ^ Luminance.GetHashCode();
		}

		#region INotifyPropertyChanged Members
		public event PropertyChangedEventHandler PropertyChanged;

		void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
		}
		#endregion INotifyPropertyChanged Members

		#region IDataErrorInfo Members
		string IDataErrorInfo.Error
		{
			get 
			{
				IDataErrorInfo iDataErrorInfo = (IDataErrorInfo)this;

				StringBuilder sb = new StringBuilder();
				string s = iDataErrorInfo["Alpha"];
				if (!string.IsNullOrEmpty(s))
					sb.Append(s);
				s = iDataErrorInfo["Hue"];
				if (!string.IsNullOrEmpty(s))
				{
					if (sb.Length > 0)
						sb.AppendFormat("{0}{1}", Environment.NewLine, s);
					else
						sb.Append(s);
				}
				s = iDataErrorInfo["Saturation"];
				if (!string.IsNullOrEmpty(s))
				{
					if (sb.Length > 0)
						sb.AppendFormat("{0}{1}", Environment.NewLine, s);
					else
						sb.Append(s);
				}
				s = iDataErrorInfo["Luminance"];
				if (!string.IsNullOrEmpty(s))
				{
					if (sb.Length > 0)
						sb.AppendFormat("{0}{1}", Environment.NewLine, s);
					else
						sb.Append(s);
				}
				return sb.ToString(); 
			}
		}

		string IDataErrorInfo.this[string columnName]
		{
			get
			{
				switch (columnName)
				{
					case "Alpha":
						if (Alpha < 0 || Alpha > 1)
							return "Alpha must fall inside [0,1]";
						break;
					case "Hue":
						if (Hue < 0 || Hue > 360)
							return "Hue must fall inside [0,360]";
						break;
					case "Saturation":
						if (Saturation < 0 || Saturation > 1)
							return "Saturation must fall inside [0,1]";
						break;
					case "Luminance":
						if (Luminance < 0 || Luminance > 1)
							return "Luminance must fall inside [0,1]";
						break;
				}
				return null;
			}
		}
		#endregion IDataErrorInfo Members
	}
}
